/*
 * F-List Pidgin - a libpurple protocol plugin for F-Chat
 *
 * Copyright 2011 F-List Pidgin developers.
 *
 * This file is part of F-List Pidgin.
 *
 * F-List Pidgin is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 2 of the License, or
 * (at your option) any later version.
 *
 * F-List Pidgin is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with F-List Pidgin.  If not, see <http://www.gnu.org/licenses/>.
 */

#include "f-list_friends.h"

#define JSON_BOOKMARK_LIST      "http://www.f-list.net/json/api/bookmark-list.php"
#define JSON_BOOKMARK_ADD       "http://www.f-list.net/json/api/bookmark-add.php"
#define JSON_BOOKMARK_REMOVE    "http://www.f-list.net/json/api/bookmark-remove.php"
#define JSON_FRIENDS_LIST       "http://www.f-list.net/json/api/friend-list.php"
#define JSON_FRIENDS_REQUEST    "http://www.f-list.net/json/api/request-send.php"
#define JSON_FRIENDS_ACCEPT     "http://www.f-list.net/json/api/request-accept.php"
#define JSON_FRIENDS_DENY       "http://www.f-list.net/json/api/request-deny.php"
#define JSON_FRIENDS_CANCEL     "http://www.f-list.net/json/api/request-cancel.php"
#define JSON_FRIENDS_REQUESTS_IN        "http://www.f-list.net/json/api/request-list.php"
#define JSON_FRIENDS_REQUESTS_OUT       "http://www.f-list.net/json/api/request-pending.php"
#define FLIST_FRIENDS_SYNC_TIMEOUT 60

#define ERROR_CANNOT_BOOKMARK "This user doesn't want to be bookmarked."

struct FListFriends_ {
    gboolean dirty;
    GList *sync_requests;
    gboolean sync_timer_active;
    guint sync_timer;
    gboolean syncing;
    
    GList *requests;
    
    GHashTable *friends;
    GHashTable *friends_new;
    
    GHashTable *cannot_bookmark;
};

typedef struct FListFriend_ {
    gchar *name;
    FListFriendStatus status;
    gint code;
    gboolean bookmarked;
} FListFriend;

typedef struct FListFriendsRequest_ {
    FListAccount *fla;
    FListWebRequestData *req_data;
    FListFriendsRequestType type;
    gchar *character;
    gint code;
    gboolean automatic;
} FListFriendsRequest;

static void flist_friend_free(gpointer data) {
    FListFriend *f = data;
    g_free(f->name);
    g_free(f);
}

static inline FListFriends *_flist_friends(FListAccount *fla) {
    return fla->flist_friends;
}
static void flist_friends_sync_timer(FListAccount *fla, guint32 timeout);

static void flist_friends_request_delete(FListFriendsRequest *req) {
    if(req->character) g_free(req->character);
    g_free(req);
}

static void flist_friends_request_cancel(FListFriendsRequest *req) {
    flist_web_request_cancel(req->req_data);
    flist_friends_request_delete(req);
}

static void _delete_request(FListFriends *flf, FListFriendsRequest *data) {
    GList *cur = flf->requests;
    while(cur) {
        if(cur->data == data) {
            flf->requests = g_list_delete_link(flf->requests, cur);
            return;
        }
        cur = cur->next;
    }
}

static void _delete_sync_request(FListFriends *flf, FListFriendsRequest *data) {
    GList *cur = flf->sync_requests;
    while(cur) {
        if(cur->data == data) {
            flf->sync_requests = g_list_delete_link(flf->sync_requests, cur);
            return;
        }
        cur = cur->next;
    }
}

static void _clear_requests(FListFriends *flf) {
    while(flf->requests) {
        flist_friends_request_cancel((FListFriendsRequest*) flf->requests->data);
        flf->requests = g_list_delete_link(flf->requests, flf->requests);
    }
}

static void _clear_sync_requests(FListFriends *flf) {
    while(flf->sync_requests) {
        flist_friends_request_cancel((FListFriendsRequest*) flf->sync_requests->data);
        flf->sync_requests = g_list_delete_link(flf->sync_requests, flf->sync_requests);
    }
    if(flf->sync_timer_active) {
        flf->sync_timer_active = FALSE;
        purple_timeout_remove(flf->sync_timer);
    }
    if(flf->syncing) {
        flf->syncing = FALSE;
        g_hash_table_destroy(flf->friends_new);
        flf->friends_new = NULL;
    }
}

//static void flist_friends_sync(FListAccount*);
//static void flist_friends_refresh(FListAccount*);

gboolean flist_friends_is_bookmarked(FListAccount* fla, const gchar* character) {
    FListFriends *flf = _flist_friends(fla);
    FListFriend *friend;
    if(!flf->friends) return FALSE;
    
    friend = g_hash_table_lookup(flf->friends, character);
    if(!friend) return FALSE;
    return friend->bookmarked;
}
FListFriendStatus flist_friends_get_friend_status(FListAccount* fla, const gchar* character) {
    FListFriends *flf = _flist_friends(fla);
    FListFriend *friend;
    if(!flf->friends) return FLIST_FRIEND_STATUS_UNKNOWN;
    
    friend = g_hash_table_lookup(flf->friends, character);
    if(!friend) return FLIST_NOT_FRIEND;
    return friend->status;
}

static void flist_friends_action_cb(FListWebRequestData* req_data, gpointer user_data,
        JsonObject *root, const gchar *error_message) {
    FListFriendsRequest *req = user_data;
    FListAccount *fla = req->fla;
    FListFriends *flf = _flist_friends(fla);
    const gchar *error;
    gchar *primary_error = NULL, *secondary_error = NULL;
    gboolean success = TRUE;
    
    if(!root) {
        purple_debug_warning(FLIST_DEBUG, "We failed our friends action. Error Message: %s\n", error_message);
        primary_error = g_strdup("Your friends request failed.");
        secondary_error = g_strdup(error_message);
        success = FALSE;
    } else {
        error = json_object_get_string_member(root, "error");
        if(error && strlen(error)) {
            if(!strcmp(error, ERROR_CANNOT_BOOKMARK)) {
                purple_debug_warning(FLIST_DEBUG, "We are unable to bookmark a character. Error Message: %s\n", error);
                success = FALSE;
            } else {
                purple_debug_warning(FLIST_DEBUG, "The server returned an unknown error for our friends action. Error Message: %s\n", error);
                success = FALSE;
            }
            primary_error = g_strdup("Your friends request returned an error.");
            secondary_error = g_strdup(error);
        }
    }
    
    if(!success && req->type == FLIST_BOOKMARK_ADD) {
        gchar *character = g_strdup(req->character);
        g_hash_table_insert(flf->cannot_bookmark, character, character);
    }
    
    if(!success && primary_error && secondary_error) {
        if(!(req->automatic)) {
            purple_notify_warning(fla->pc, "F-List Friends Request", primary_error, secondary_error);
        } 
    }
    if(primary_error) g_free(primary_error);
    if(secondary_error) g_free(secondary_error);
    
    _delete_request(flf, req);
    flist_friends_request_delete(req);
    
    if(!flf->requests) {
        flist_friends_sync_timer(fla, 0);
    }
}

void flist_blist_node_action(PurpleBlistNode *node, gpointer data) {
    PurpleBuddy *b = (PurpleBuddy*) node;
    PurpleAccount *pa = purple_buddy_get_account(b);
    PurpleConnection *pc;
    FListAccount *fla;
    FListFriends *flf;
    FListFriendsRequestType type = GPOINTER_TO_INT(data);
    GHashTable *args;
    FListFriend *friend;
    FListFriendsRequest *req;
    const gchar *name = purple_buddy_get_name(b);
    
    g_return_if_fail((pc = purple_account_get_connection(pa)));
    g_return_if_fail((fla = pc->proto_data));
    flf = _flist_friends(fla);
    friend = g_hash_table_lookup(flf->friends, name);
    args = flist_web_request_args(fla);
    
    req = g_new0(FListFriendsRequest, 1);
    req->fla = fla;
    req->character = g_strdup(name);
    req->type = type;
    req->automatic = FALSE;
    
    switch(type) {
    case FLIST_FRIEND_REQUEST:
        g_hash_table_insert(args, "source_name", g_strdup(fla->character));
        g_hash_table_insert(args, "dest_name", g_strdup(name));
        req->req_data = flist_web_request(JSON_FRIENDS_REQUEST, args, flist_friends_action_cb, req);
        break;
    case FLIST_FRIEND_AUTHORIZE:
        if(!friend) break;
        g_hash_table_insert(args, "request_id", g_strdup_printf("%d", friend->code));
        req->code = friend->code;
        req->req_data = flist_web_request(JSON_FRIENDS_REQUEST, args, flist_friends_action_cb, req);
        break;
    case FLIST_FRIEND_DENY:
        if(!friend) break;
        g_hash_table_insert(args, "request_id", g_strdup_printf("%d", friend->code));
        req->code = friend->code;
        req->req_data = flist_web_request(JSON_FRIENDS_DENY, args, flist_friends_action_cb, req);
        break;
    case FLIST_FRIEND_CANCEL:
        if(!friend) break;
        g_hash_table_insert(args, "request_id", g_strdup_printf("%d", friend->code));
        req->code = friend->code;
        req->req_data = flist_web_request(JSON_FRIENDS_CANCEL, args, flist_friends_action_cb, req);
        break;
    case FLIST_BOOKMARK_ADD:
        if(g_hash_table_lookup(flf->cannot_bookmark, name)) break; /* We cannot bookmark some users. Move on. */
        g_hash_table_insert(args, "name", g_strdup(name));
        req->req_data = flist_web_request(JSON_BOOKMARK_ADD, args, flist_friends_action_cb, req);
        break;
    case FLIST_BOOKMARK_REMOVE:
        g_hash_table_insert(args, "name", g_strdup(name));
        req->req_data = flist_web_request(JSON_BOOKMARK_REMOVE, args, flist_friends_action_cb, req);
        break;
    default: break;
    }
    
    g_hash_table_destroy(args);
    if(req->req_data) {
        _clear_sync_requests(flf);
        flf->requests = g_list_prepend(flf->requests, req);
    } else {
        flist_friends_request_delete(req);
    }
}

void flist_friends_add_buddy(FListAccount *fla, const gchar* character) {
//    FListFriends *flf = _flist_friends(fla);
}

void flist_friends_remove_buddy(FListAccount *fla, const gchar* character) {
//    FListFriends *flf = _flist_friends(fla);
}

static void flist_friends_add_buddy_real(FListAccount *fla, const gchar *character, gboolean bm) {
    PurpleGroup *bm_group, *f_group;
    GSList *buddies;
    
    //If we're adding a friend and he's listed under bookmarks, remove him from bookmarks.
    bm_group = flist_get_bookmarks_group(fla);
    f_group = flist_get_friends_group(fla);
    buddies = purple_find_buddies(fla->pa, character);
    if(buddies && !bm) {
        while(buddies) {
            PurpleBuddy *b = buddies->data;
            if(purple_buddy_get_group(b) != bm_group) {
                break;
            }
            buddies = g_slist_delete_link(buddies, buddies);
            purple_blist_remove_buddy(b);
        }
    }
    
    //Add the friend in the proper spot.
    if(!buddies) {
        PurpleBuddy *b = purple_buddy_new(fla->pa, character, NULL);
        purple_blist_add_buddy(b, NULL, bm ? bm_group : f_group, NULL);
        flist_update_friend(fla->pc, character, TRUE, TRUE);
    }
    
    g_slist_free(buddies);
}

static GHashTable *flf_dupe(FListFriends *flf) {
    GHashTable *ret = g_hash_table_new_full((GHashFunc) flist_str_hash, (GEqualFunc) flist_str_equal, NULL, NULL);
    GHashTableIter i;
    gpointer key, value;
    
    g_hash_table_iter_init(&i, flf->friends);
    while(g_hash_table_iter_next(&i, &key, &value)) {
        g_hash_table_insert(ret, key, value);
    }
    
    return ret;
}

static void flist_friends_add(FListAccount *fla, const gchar *name) {
/*    FListFriends *flf = _flist_friends(fla);
    GHashTable *args = flist_web_request_args(fla);
    g_hash_table_insert(args, "name", g_strdup(name));
    g_hash_table_destroy(args);*/
}

static void flist_friends_remove(FListAccount *fla, const gchar *name) {
/*    FListFriends *flf = _flist_friends(fla);
    GHashTable *args = flist_web_request_args(fla);
    g_hash_table_insert(args, "name", g_strdup(name));
    flf->req_data = flist_web_request(fla, JSON_BOOKMARK_REMOVE, args, flist_bookmark_cb, NULL);
    g_hash_table_destroy(args);*/
}

static void flist_postsync(FListAccount *fla) {
    FListFriends *flf = _flist_friends(fla);
    flist_friends_sync_timer(fla, FLIST_FRIENDS_SYNC_TIMEOUT);
    
    if(flf->friends) g_hash_table_destroy(flf->friends);
    
    flf->friends = flf->friends_new;
    flf->friends_new = NULL;
    
    //TODO: Synchronize Friends
    //TODO: Synchronize Bookmarks
}

static void flist_handle_friends_list(FListAccount *fla, JsonObject *root, FListFriendsRequestType type) {
    FListFriends *flf = _flist_friends(fla);
    JsonArray *array;
    int index, len;
    gboolean in = type == FLIST_FRIENDS_REQUEST_IN;
    FListFriendStatus status;
    switch(type) {
        case FLIST_FRIENDS_LIST: status = FLIST_MUTUAL_FRIEND; break;
        case FLIST_FRIENDS_REQUEST_IN: status = FLIST_PENDING_IN_FRIEND; break;
        case FLIST_FRIENDS_REQUEST_OUT: status = FLIST_PENDING_OUT_FRIEND; break;
        default: status = FLIST_FRIEND_STATUS_UNKNOWN;
    }
    
    array = json_object_get_array_member(root, type == FLIST_FRIENDS_LIST ? "friends" : "requests");
    len = json_array_get_length(array);
    for(index = 0; index < len; index++) {
        JsonObject *o = json_array_get_object_element(array, index);
        const gchar *source = json_object_get_string_member(o, !in ? "source" : "dest");
        const gchar *dest = json_object_get_string_member(o, !in ? "dest" : "source");
        gint id = json_object_get_int_member(o, "id");
        FListFriend *friend;
        if(flist_strcmp(fla->character, source)) continue; //We are only interested in friends of our character.
        friend = g_hash_table_lookup(flf->friends_new, dest);
        if(!friend) {
            friend = g_new0(FListFriend, 1);
            friend->name = g_strdup(dest);
            g_hash_table_replace(flf->friends_new, friend->name, friend);
        }
        friend->status = status;
        friend->code = id;
    }
}

static void flist_handle_bookmark_list(FListAccount *fla, JsonObject *root) {
    FListFriends *flf = _flist_friends(fla);
    JsonArray *array;
    int index, len;
    
    array = json_object_get_array_member(root, "characters");
    len = json_array_get_length(array);
    for(index = 0; index < len; index++) {
        const gchar *character = json_array_get_string_element(array, index);
        FListFriend *friend;
        
        friend = g_hash_table_lookup(flf->friends_new, character);
        if(!friend) {
            friend = g_new0(FListFriend, 1);
            friend->name = g_strdup(character);
            g_hash_table_replace(flf->friends_new, friend->name, friend);
            friend->status = FLIST_NOT_FRIEND;
        }
        friend->bookmarked = TRUE;
    }
}

static void flist_friends_cb(FListWebRequestData *req_data, gpointer user_data,
        JsonObject *root, const gchar *error_message) {
    FListFriendsRequest *req = user_data;
    FListAccount *fla = req->fla;
    FListFriends *flf = _flist_friends(fla);
    const gchar *error;
    FListFriendsRequestType type = req->type;
    gboolean success = TRUE;
    
    _delete_sync_request(flf, req);
    flist_friends_request_delete(req);
    
    if(error_message) {
        purple_debug_info(FLIST_DEBUG, "We have failed a friends list request. Error Message: %s\n", error_message);
        success = FALSE;
    } else {
        error = json_object_get_string_member(root, "error");
        if(error && strlen(error)) {
            purple_debug_info(FLIST_DEBUG, "We requested a friends list, but it returned an error. Error Message: %s\n", error);
            success = FALSE;
        }
    }

    if(!success) { /* We failed to sync ... reschedule later. */
        _clear_sync_requests(flf);
        flist_friends_sync_timer(fla, FLIST_FRIENDS_SYNC_TIMEOUT);
        return;
    }
    
    switch(type) {
    case FLIST_BOOKMARKS_LIST:
        flist_handle_bookmark_list(fla, root);
        break;
    case FLIST_FRIENDS_LIST:
        flist_handle_friends_list(fla, root, type);
        break;
    case FLIST_FRIENDS_REQUEST_IN:
        flist_handle_friends_list(fla, root, type);
        break;
    case FLIST_FRIENDS_REQUEST_OUT:
        flist_handle_friends_list(fla, root, type);
        break;
    default: //How did this happen?!
        break;
    }
        
    if(!flf->sync_requests) {
        flf->syncing = FALSE;
        flist_postsync(fla);
    }
}

static gboolean flist_friends_sync_timer_cb(gpointer data) {
    FListAccount *fla = data;
    FListFriends *flf = _flist_friends(fla);
    FListFriendsRequest *req;
    GHashTable *args = flist_web_request_args(fla);
    
    flf->friends_new = g_hash_table_new_full((GHashFunc) flist_str_hash, (GEqualFunc) flist_str_equal, NULL, flist_friend_free);
    
    /* Request all of the pages. */
    req = g_new0(FListFriendsRequest, 1);
    req->fla = fla; req->automatic = TRUE; req->type = FLIST_BOOKMARKS_LIST;
    req->req_data = flist_web_request(JSON_BOOKMARK_LIST, args, flist_friends_cb, req);
    flf->sync_requests = g_list_prepend(flf->sync_requests, req);
    
    req = g_new0(FListFriendsRequest, 1);
    req->fla = fla; req->automatic = TRUE; req->type = FLIST_FRIENDS_LIST;
    req->req_data = flist_web_request(JSON_FRIENDS_LIST, args, flist_friends_cb, req);
    flf->sync_requests = g_list_prepend(flf->sync_requests, req);
    
    req = g_new0(FListFriendsRequest, 1);
    req->fla = fla; req->automatic = TRUE; req->type = FLIST_FRIENDS_REQUEST_IN;
    req->req_data = flist_web_request(JSON_FRIENDS_REQUESTS_IN, args, flist_friends_cb, req);
    flf->sync_requests = g_list_prepend(flf->sync_requests, req);
    
    req = g_new0(FListFriendsRequest, 1);
    req->fla = fla; req->automatic = TRUE; req->type = FLIST_FRIENDS_REQUEST_OUT;
    req->req_data = flist_web_request(JSON_FRIENDS_REQUESTS_OUT, args, flist_friends_cb, req);
    flf->sync_requests = g_list_prepend(flf->sync_requests, req);
    
    flf->sync_timer_active = FALSE;
    flf->syncing = TRUE;
    
    g_hash_table_destroy(args);
    return FALSE;
}

static void flist_friends_sync_timer(FListAccount *fla, guint32 timeout) {
    FListFriends *flf = _flist_friends(fla);
    _clear_sync_requests(flf);
    flf->sync_timer_active = TRUE;
    flf->sync_timer = purple_timeout_add_seconds(timeout, (GSourceFunc) flist_friends_sync_timer_cb, fla);
}

void flist_friends_login(FListAccount *fla) {
    flist_friends_sync_timer(fla, 0);
}

void flist_friends_load(FListAccount *fla) {
    FListFriends *flf;
    fla->flist_friends = g_new0(FListFriends, 1);
    flf = _flist_friends(fla);
    
    flf->cannot_bookmark = g_hash_table_new_full((GHashFunc) flist_str_hash, (GEqualFunc) flist_str_equal, NULL, g_free);
}

void flist_friends_unload(FListAccount *fla) {
    FListFriends *flf = _flist_friends(fla);
    
    if(flf->cannot_bookmark) g_hash_table_destroy(flf->cannot_bookmark);
    if(flf->friends) g_hash_table_destroy(flf->friends);
    
    /* Cancel all of our pending requests ... */
    _clear_sync_requests(flf);
    _clear_requests(flf);
    
    g_free(fla->flist_friends);
}










